package ru.emms.codemeter.metrics;


import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.peer.PeerFactory;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiManager;
import ru.emms.codemeter.DataBaseManager;
import ru.emms.codemeter.metrics.calculators.MetricCalculator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

/**
 * Created by IntelliJ IDEA.
 * User: dima.sam
 * Date: 28.07.2010
 * Time: 11:53:57
 * Класс, ответственный за вычисление метрик по выбранным модулям проекта.
 */
public class MetricsManager {

    private Project project;
    /**
     * проект под svn и исходники были модифицированы
     */
    public static final long MODIFIED = -1L;
    //
    /**
     * проект не под svn
     */
    public static final long LOCAL = -2L;
    private static Logger log = Logger.getInstance(MetricsManager.class.getName());
    private DataBaseManager dataBaseManager;

    public Project getProject() {
        return project;
    }

    public MetricsManager(Project project) {
        this.project = project;
        dataBaseManager = new DataBaseManager();

    }

    public DataBaseManager getDataBaseManager() {
        return dataBaseManager;
    }

    /**
     * возвращает вычисленные метрики metric для проекта project,
     * модуля module и версии version списка версий versions
     *
     * @param project  текущий проект
     * @param module   текущий модуль проекта
     * @param versions список версий
     * @param metricCalculators  список метрик, которые нужно вычислить
     * @return возвращает список пар (версия,массив метрик) Версия -1 означает метрики для текущего измененного модуля
     *         (не зафиксированного в VCS).
     */
    public Map<Long, List<Long>> calculateMetricsByModule(Project project, Module module, List<Long> versions, List<MetricCalculator> metricCalculators) {
        Map<Long, List<Long>> calculatedValues = loadFromDatabase(module, versions, metricCalculators);
        Collection<PsiFile> sources = getSources(module);
        if (versions.contains(LOCAL)) {
            log.info("локальная версия");
            //следующее приведение типа допустимо, т.к. PsiFile - подкласс PsiElement
            List<Long> newRow = calculateMetrics(metricCalculators, sources);
            //будем использовать LOCAL для локальной версии
            calculatedValues.put(LOCAL, newRow);
        }
        if (hasAnyChanged(sources)) {
            log.info("есть изменения");
            //следующее приведение типа допустимо, т.к. PsiFile - подкласс PsiElement
            List<Long> newRow = calculateMetrics(metricCalculators, sources);
            //будем использовать MODIFIED для текущей (измененной) версии
            calculatedValues.put(MODIFIED, newRow);
        } else if (versions.contains(currentVersion(module))) {
            Long version = currentVersion(module);
            log.debug("нет изменений");
            log.debug("текущая версия: " + version);
            String projectName = project.getName();
            String moduleName = module.getName();
            boolean versionInDatabase = calculatedValues.keySet().contains(version);
            if (!versionInDatabase) {
                log.debug("пересчитываем все требуемые метрики");
                List<Long> newRow = calculateMetrics(metricCalculators, sources);
                calculatedValues.put(version, newRow);
                for (int i = 0; i < newRow.size(); i++) {
                    dataBaseManager.insertData(projectName, moduleName, version, metricCalculators.get(i).toString(), newRow.get(i));
                    calculatedValues.put(version, newRow);
                }
            } else {
                List<Long> metricFromDataBase = calculatedValues.get(version);
                List<Long> newRow = new ArrayList<Long>();
                int metricCount = metricCalculators.size();
                for (int i = 0; i < metricCount; i++) {
                    if (metricFromDataBase.get(i) == null) {
                        newRow.add(metricCalculators.get(i).calculate(sources));
                        //записываем пересчитанную метрику в базу
                        dataBaseManager.insertData(projectName, moduleName, version, metricCalculators.get(i).toString(), newRow.get(i));
                        log.debug("пересчитываем метрику " + metricCalculators.get(i) + "= " + newRow.get(i));
                    } else {
                        log.debug("добавляем метрику " + metricCalculators.get(i) + "= " + metricFromDataBase.get(i));
                        newRow.add(metricFromDataBase.get(i));
                    }
                }
                calculatedValues.put(version, newRow);
            }
        }
        return calculatedValues;
    }

    /**
     * Загрузка ранее вычисленных метрик из базы
     *
     * @param module   модуль
     * @param versions список версий
     * @param metricCalculators  список метрик
     * @return карту "версия -> список метрик". В списке метрик значения находятся в том же порядке,
     *         что и в metrics. Значения в списке могут быть null, если в базе нет значения для соответствующей метрики
     */
    private Map<Long, List<Long>> loadFromDatabase(Module module, List<Long> versions, List<MetricCalculator> metricCalculators) {
        //версия -> список метрик
        Map<Long, List<Long>> metricValuesMap = new HashMap<Long, List<Long>>();
        for (Long version : versions) {
            //для каждой версии выбираем метрики
            List<Metric> allMetricsValues =
                dataBaseManager.selectByNameAndVersion(module.getProject().getName(), module.getName(), version);
            //..и преобразуем их в список в соответствии с порядком из metrics
            List<Long> calculatedRow = new ArrayList<Long>();
            if (allMetricsValues.size() > 0) {
                for (MetricCalculator metricCalculator : metricCalculators) {
                    calculatedRow.add(findValue(allMetricsValues, metricCalculator.toString()));
                }
                //добавляем этот список в результирующую карту
                metricValuesMap.put(version, calculatedRow);
            }
        }
        return metricValuesMap;
    }

    private List<Long> calculateMetrics(List<MetricCalculator> metricCalculators, Collection<PsiFile> sources) {
        List<Long> metricValues = new ArrayList<Long>();
        for (MetricCalculator metricCalculator : metricCalculators) {
            metricValues.add(metricCalculator.calculate(sources));
        }
        return metricValues;
    }

    /**
     * возвращает значение метрики metricName среди metricsValues
     *
     * @param metricsValues коллекция метрик
     * @param metricName    имя искомой метрики
     * @return значение искомой метрики, или null, если метрика не найдена
     */
    public Long findValue(List<Metric> metricsValues, String metricName) {
        for (Metric metricsValue : metricsValues) {
            if (metricsValue.getMetricType().equals(metricName)) {
                return metricsValue.getMetricValue();
            }
        }
        log.info("findValue - значение метрики не найдено");
        return null;
    }

    /**
     * возвращает вычисленные метрики metric для проекта project,
     * массива модулей modules, списка версий versions
     *
     * @param project  - текущий проект
     * @param modules  - список модулей проекта
     * @param versions - список версий
     * @param metricCalculators  - список метрик, которые нужно вычислить
     * @return возвращает Карту с парами (имя модуля,(версия,[метрики]))
     */
    public Map<String, Map<Long, List<Long>>> сalculateMetrics(Project project, List<Module> modules, List<Long> versions, List<MetricCalculator> metricCalculators) {
        Map<String, Map<Long, List<Long>>> result = new HashMap<String, Map<Long, List<Long>>>();
        Map<Long, List<Long>> calculatedValues;
        for (Module module : modules) {
            calculatedValues = calculateMetricsByModule(project, module, versions, metricCalculators);
            result.put(module.getName(), calculatedValues);
        }
        return result;
    }

    /**
     * Возвращает коллекцию исходников, содержащихся в модуле module
     *
     * @param module модуль проекта
     * @return возвращает коллекцию исходников данного проекта и модуля
     */
    public static Collection<PsiFile> getSources(Module module) {
        Project project = module.getProject();
        ProjectRootManager rootManager = ProjectRootManager.getInstance(project);
        VirtualFile[] sourceRoots = rootManager.getContentSourceRoots();
        PsiManager psiManager = PsiManager.getInstance(project);
        Collection<PsiFile> sources = new ArrayList<PsiFile>();
        for (VirtualFile root : sourceRoots) {
            log.debug("source root: " + root.getPath());
            if (ModuleUtil.moduleContainsFile(module, root, false)) {
                PsiDirectory directory = psiManager.findDirectory(root);
                sources.addAll(javaFilesIn(directory));
            }
        }
        return sources;
    }

    /**
     * Получение java-исходников, лежащих в каталоге.
     *
     * @param directory каталог поиска
     * @return коллекцию исходников, лежащих в каталоге и его подкаталогах.
     */
    private static Collection<PsiFile> javaFilesIn(PsiDirectory directory) {
        Collection<PsiFile> files = new ArrayList<PsiFile>();
        for (PsiFile file : directory.getFiles()) {
            if (file instanceof PsiJavaFile) {
                files.add(file);
            }
        }
        for (PsiDirectory file : directory.getSubdirectories()) {
            files.addAll(javaFilesIn(file));
        }
        log.debug("количество java файлов в каталоге " + directory.getName() + ": " + files.size());
        return files;
    }

    /**
     * возвращает список модулей текущего проекта
     *
     * @return возвращает список модулей  see #6181 Добавлен метод, определяющий, были ли изменены исходники модуля
     */
    public List<Module> getAllModules() {
        return Arrays.asList(ModuleManager.getInstance(project).getModules());
    }

    /**
     * Извлекает из базы список версий для данного проекта, модуля
     *
     * @param projectName имя проекта
     * @param modules     имя модуля
     * @return список версий
     */
    public ArrayList<Long> getAllVersion(String projectName, List<Module> modules) {
        TreeSet<Long> setVersions = new TreeSet<Long>(new Comparator<Long>() {
            @Override
            //метод переопределяется так, чтобы версия MODIFIED(отрицательная) была последней в списке
            public int compare(Long value1, Long value2) {
                //убираем знаки сравниваемых величин, чтобы отрицательные числа были большими
                value1 &= Long.MAX_VALUE;
                value2 &= Long.MAX_VALUE;
                return value1.compareTo(value2);
            }
        });
        for (Module module : modules) {
            List<Metric> metricsValues = dataBaseManager.selectVersion(projectName, module.getName());
            for (Metric value : metricsValues) {
                setVersions.add(value.getVersionNumber());
            }
            long current = currentVersion(module);
            setVersions.add(current);
            if (current != LOCAL) {
                if (hasAnyChanged(getSources(module))) {
                    setVersions.add(MODIFIED);
                }
            }

            log.debug("список версий для модуля " + module.getName() + " : " + setVersions);
        }
        return new ArrayList<Long>(setVersions);
    }

    /**
     * вынимает из svn текущую версию модуля
     *
     * @param module модуль
     * @return номер версии
     */
    public static long currentVersion(Module module) {
        VirtualFile fileToCheckOut = module.getModuleFile().getParent();
        AbstractVcs vcs = ProjectLevelVcsManager.getInstance(module.getProject()).getVcsFor(fileToCheckOut);
        FilePath path = PeerFactory.getInstance().getVcsContextFactory().createFilePathOn(fileToCheckOut);
        Long value = null;
        boolean isUnderVcs = vcs != null && vcs.fileIsUnderVcs(path);
        if (isUnderVcs) {
            String stringValue = vcs.getDiffProvider().getLastRevision(path).getNumber().asString();
            try {
                value = Long.valueOf(stringValue);
            } catch (NumberFormatException e) {
                log.debug("ошибка преобразования версии", e);
            }
        } else {
            value = LOCAL;
        }
        log.debug("текущая версия модуля: " + module.getName() + " : " + value);
        return value;
    }

    /**
     * Проверка, был ли изменен какой-нибудь файл из коллекции
     *
     * @param files коллекция файлов
     * @return true, если хотя бы один файл из коллекции был изменен
     */
    private static boolean hasAnyChanged(Collection<PsiFile> files) {

        for (PsiFile file : files) {
            boolean fileChanged = file.getFileStatus() != FileStatus.NOT_CHANGED;
            if (fileChanged) {
                log.debug("Проверка: " + file + "был изменен");
                return true;
            }
        }
        return false;
    }
}



